iT邦幫忙

2023 iThome 鐵人賽

DAY 29
0

終於到了最後一個範例,在 第一天的文章 - 前言 提到最近工作上使用到 opencv.js,所以才有這 30 天的 web worker 系列文,有始有終,今天就讓我們來完成一開始打算做的事情 - 在 web worker 中使用 opencv.js

什麼是 opencv.js ?

前情提要一下什麼是 opencv.js

opencv.jsOpenCV(Open Source Computer Vision Library)JavaScript 版本。 它是 OpenCV 函式庫的子項目,透過使用 Emscripten 技術,將原始的 C++ 程式碼編譯成 JavaScript,使得開發者可以在瀏覽器中直接執行 OpenCV 的功能。 OpenCV 提供了一些圖像處理的功能,例如:濾波、邊緣檢測、顏色轉換、人臉識別等

進一步瞭解可以參考 官方 opencv.js 教學

範例

目的

結合 web workeropencv.js,比較 有無使用 web worker 效能的差異

說明

  1. 假設圖片中較亮的地方是可能的恆星或星系所在位置,可以利用 opencv 的輪廓檢測算法,找出這些星星的位置並以綠色亮點標記出來
  2. 畫面上方有個 拖拉元件(slider),隨著拖拉的移動,判定找到的星星數會越多
  3. 在拖拉的過程中都會呼叫 opencv 的算法,預期這部分的運算量比較大,可能會佔用到主線程的資源導致畫面卡頓
  4. 當勾選上方的 使用 web worker 後會將 opencv 的算法移到 web worker 中做處理
  5. 最後比較 有無使用 web worker 對於效能的影響

範例 Demo
圖片很大,第一次進去可能會載入比較久,請耐心等候
https://ithelp.ithome.com.tw/upload/images/20231013/20162687RMYXERC7zq.png

Step 1. 偵測使用者拉動的 slider

在使用者拉動 slider 時,會根據改變的值 (0~100%) 當作參數去尋找圖中的星星

document.querySelector('#slider').addEventListener('change', async (e) => {
  const checkbox = document.querySelector('#use-worker');
  const isUsingWorker = checkbox.checked;

  // 拖動範圍 0 ~ 100%
  const percent = e.detail;

  if (isUsingWorker) {
    // 使用 web worker 尋找星星
    await findStarsWithWorker(percent);
  } else {
    // 不使用 web worker 尋找星星
    findStarsWithoutWorker(percent);
  }
});

Step 2-1. 不使用 web worker 尋找星星 (findStarsWithoutWorker)

先來看 不使用 worker 找星星 的部分

opencv 中可以使用 cv.imread(),將圖片或是 canvas 讀進來,這裡帶入 canvas 元件 id (#canvasInput) 可以把 canvas 中的圖像載入進 opencv 裡做使用

接著主要是利用 cv.findContours,找尋星星的輪廓標記成綠色亮點,這部分因為不是今天強調的重點就不仔細說明了,大致上類似以下兩篇文章的實作方式:

當找出星星後,會將其標記成綠色亮點,接著將 結果圖像(outputMat) 利用 cv.imshow(),顯示在 canvas 上(#canvasOutput)

export const findStarsWithoutWorker = (percent = 0) => {
  // 讀入原始星空圖
  const inputMat = cv.imread('canvasInput');

  // 初始 percent 為 0,不做任何處理回傳原始圖
  if (!percent) {
    cv.imshow('canvasOutput', inputMat);
    inputMat.delete();
    return;
  }

  // 使用 opencv 尋找星星邏輯 (省略...)
  // ...

  // 在瀏覽器中顯示結果圖像
  cv.imshow('canvasOutput', outputMat);
};

Step 2-2. 使用 web worker 尋找星星 (findStarsWithWorker)

由於使用 cv.imread() 載入圖像後回傳的是 opencv 特有的 cv.Mat 資料格式,而 cv.Mat 是無法經由 postMessagestructuredClone 算法傳遞到 worker 線程中的

// 主線程
const inputMat = cv.imread('canvasInput');
worker.postMessage(inputMat);

// worker 線程
self.onmessage = (e) => {
    // cv.Mat 資料無法被複製
    console.log(e.data); // {}
}

傳遞 圖像資料(ImageData) 到 worker 線程

因此我們需要先將 cv.Mat 使用 matToImageData 函式轉換為 第 14 天所提到的 ImageData,才能使用 postMessage 傳遞資料,注意這裡 postMessage 有設定第二個參數將 ImageDatabuffer 轉移到 worker 線程,略過複製讓資料的傳遞更快

const matToImageData = (mat) => {
  return new ImageData(new Uint8ClampedArray(mat.data), mat.cols, mat.rows);
};

export const findStarsWithWorker = (percent) => {
  return new Promise((resolve) => {
    const inputMat = cv.imread('canvasInput');
    const imageData = matToImageData(inputMat);
    
    // 釋放記憶體
    inputMat.delete();

    worker.postMessage(
      {
        imageData,
        percent
      },
      [imageData.data.buffer]
    );
  });
};

釋放 cv.Mat 記憶體

以上程式碼有一行 inputMat.delete() 拿來釋放不會用到的 cv.Mat 記憶體,我一開始忘記加到這行,當 slider 拖拉久了之後,可以看到 console 一直噴錯,不斷印出一串數字,當加了 inputMat.delete() 正確釋放記憶體後就沒有這個問題了
https://ithelp.ithome.com.tw/upload/images/20231013/20162687vdND6aNAxn.png

worker 線程處理圖像資料

worker 線程接收到 ImageData 後需要轉換回 cv.Mat 的形式,才能使用 opencv 運算,這裡我們可以使用 opencv 內建的 cv.matFromImageData() 轉換,接下來的部分跟 不使用 web worker 尋找星星(findStarsWithoutWorker) 一樣,差別只在運算完後需要再把 cv.Mat 轉換成 ImageData 傳回主線程

// worker 線程
self.onmessage = (e) => {
  findStarsInWorker(e.data);
};

function findStarsInWorker({ imageData, percent = 0 }) {
  const inputMat = cv.matFromImageData(imageData);
  
  if (!percent) {
    // 將處理後的圖像資料傳回主線程
    const processedImageData = matToImageData(inputMat);
    self.postMessage({ imageData: processedImageData }, [
      processedImageData.data.buffer
    ]);

    // 釋放記憶體
    inputMat.delete();
    return;
  }
  
  // 使用 opencv 尋找星星邏輯 (省略...)
  // ...

  // 將處理後的圖像資料傳回主線程
  const processedImageData = matToImageData(outputMat);
  self.postMessage({ imageData: processedImageData }, [
    processedImageData.data.buffer
  ]);  
}

主線程接收圖像資料

最終主線程接收到 ImageData 後使用 putImageData 就可以畫在 canvas 上

export const findStarsWithWorker = (percent) => {
  return new Promise((resolve) => {
    worker.onmessage = (e) => {
      const outputCanvas = document.querySelector('#canvasOutput');
      outputCanvas.getContext('2d').putImageData(e.data.imageData, 0, 0);

      resolve();
    };
  });
};

結果

https://ithelp.ithome.com.tw/upload/images/20231013/201626878td4z8dlk3.png

https://ithelp.ithome.com.tw/upload/images/20231013/20162687CJ3OQyGQ1F.png

  1. 不論 有無開啟 worker,運算的時間其實差異不大,在我的電腦上測試兩者跑的時間大約都在 10~30ms
  2. 但 10~30ms 的運算時間也足夠讓使用者在拖拉時會感到卡頓了,我們可以發現在 沒有使用 worker 的狀況下,偶爾拖拉時還是會有卡頓的狀況,但 使用 worker 時,因為 opencv 有關的操作都移到 worker 線程中運算了,所以整個拖拉的操作會很順暢

結論

使用 opencv.js 可以在網頁上做到圖像辨識、邊緣偵測等許多功能,但也因為其對影像處理的強大,使用 opencv.js 常會需要更多的運算時間,當運算的時間長到會影響到畫面上的操作時,可以考慮將相關的運算移到 worker 線程中處理,優化使用者在網頁上的操作體驗

Reference

Getting Started with Images
学习opencv.js(1)图像入门
Contours : Getting Started
二值化黑白影像


上一篇
使用多個 Web worker 進行平行運算 - workerpool
下一篇
Web worker 系列文 - 總結
系列文
網頁的另一個大腦:從基礎到進階掌握 Web Worker 技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言